import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import indi.hjhk.exception.ExceptionSerializer;
import indi.hjhk.log.Logger;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
import java.util.Stack;

public class ArcadeTSDBClient {
    public static final String DEFAULT_SERVER_HOST = "127.0.0.1";
    public static final int DEFAULT_SERVER_PORT = 8809;
    public static final int SHOW_MESSAGE_LENGTH = 1000;

    public static String toPrettyPrintJSON(String json){
        StringBuilder strBuilder = new StringBuilder();
        StringBuilder prefix = new StringBuilder();
        boolean insideString = false;
        boolean trope = false;
        for (char ch : json.toCharArray()){
            switch (ch) {
                case '{', '[' -> {
                    strBuilder.append(ch);
                    if (!insideString) {
                        prefix.append('\t');
                        strBuilder.append('\n').append(prefix);
                    }
                }
                case '}', ']' -> {
                    if (!insideString) {
                        prefix.deleteCharAt(prefix.length() - 1);
                        strBuilder.append('\n').append(prefix);
                    }
                    strBuilder.append(ch);
                }
                case '"' -> {
                    if (!trope)
                        insideString = !insideString;
                    strBuilder.append(ch);
                }
                case ',' -> {
                    strBuilder.append(ch);
                    if (!insideString){
                        strBuilder.append('\n').append(prefix);
                    }
                }
                default -> strBuilder.append(ch);
            }
            if (trope)
                trope = false;
            else if (ch == '\\')
                trope = true;
        }
        return strBuilder.toString();
    }

    final String hostIP;
    final int hostPort;
    final Logger logger = Logger.getPureLogger("TSDBClient");;
    Socket socket = null;
    PrintWriter writer = null;
    BufferedReader reader = null;

    public ArcadeTSDBClient(){
        this(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
    }

    public ArcadeTSDBClient(String hostIP){
        this(hostIP, DEFAULT_SERVER_PORT);
    }

    public ArcadeTSDBClient(String hostIP, int hostPort) {
        this.hostIP = hostIP;
        this.hostPort = hostPort;

        logger.logSilent("ArcadeTSDB client log");
        connect();
    }

    public void connect(){
        if (socket == null || socket.isClosed()) {
            try {
                socket = new Socket(hostIP, hostPort);
                writer = new PrintWriter(socket.getOutputStream());
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            } catch (IOException e) {
                logger.logOnStderr("failed to connect to server at %s:%d", hostIP, hostPort);
                logger.logOnStderr(ExceptionSerializer.serializeAll(e));
                close();
            }
            logger.logOnStdout("connected to server at %s:%d", hostIP, hostPort);
        }
    }

    public void close(){
        sendClose();
        try {
            if (reader != null)
                reader.close();
            if (writer != null)
                writer.close();
            if (socket != null)
                socket.close();
        }catch (IOException ignored){}
        logger.logOnStdout("connection to %s:%d closed", hostIP, hostPort);
    }

    public void sendClose(){
        writer.write("close\n");
        writer.flush();
    }

    /**
     * send json message to server, return until result received
     * @param json json object to send
     * @return result from server, null if failed
     */
    public String sendJsonAndWaitResult(JSONObject json){
        if (socket == null || socket.isClosed())
            return null;

        String msg = json.toJSONString();
        logger.logSilent("to server: %s", (msg.length() > SHOW_MESSAGE_LENGTH ? msg.substring(0, SHOW_MESSAGE_LENGTH) + " ...(total " + msg.length() + " characters)" : msg));
        writer.write(msg + "\n");
        writer.flush();

        String ret;
        try {
            ret = reader.readLine();
        } catch (IOException e) {
            logger.logOnStderr("failed to read response from server");
            return null;
        }

        if (ret != null) {
            logger.logSilent("from server: %s", (ret.length() > SHOW_MESSAGE_LENGTH ? ret.substring(0, SHOW_MESSAGE_LENGTH) + " ...(total " + ret.length() + " characters)" : ret));
        } else {
            logger.logOnStderr("socket closed from server");
        }

        return ret;
    }

    public static void main(String[] args) {
        Logger logger = Logger.getPureLogger("TSDBClient");
        logger.logSilent("ArcadeTSDB client log");

        ArcadeTSDBClient client = new ArcadeTSDBClient();

        String line;
        Stack<Character> bracketStack = new Stack<Character>();
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.println("input a json or 'exit' to exit:");
            line = sc.nextLine();
            while (line.length() == 0)
                line = sc.nextLine();
            if (line.equals("exit")) break;

            if (!line.startsWith("{")){
                System.out.println("json should start with '{'");
                continue;
            }

            boolean isJsonValid = true;
            StringBuilder strJSON = new StringBuilder("{");
            bracketStack.clear();
            bracketStack.push('{');
            line = line.substring(1);
            while (true){
                strJSON.append(line);
                for (char ch : line.toCharArray()){
                    if (bracketStack.empty()){
                        System.out.println("json should end with '}'");
                        isJsonValid = false;
                        break;
                    }
                    switch (ch){
                        case '{':
                        case '[':
                            bracketStack.push(ch);
                            break;
                        case '}':
                            if (bracketStack.empty() || bracketStack.pop() != '{'){
                                System.out.println("bracket mismatch with '}'");
                                isJsonValid = false;
                            }
                            break;
                        case ']':
                            if (bracketStack.empty() || bracketStack.pop() != '['){
                                System.out.println("bracket mismatch with ']'");
                                isJsonValid = false;
                            }
                            break;
                    }
                    if (!isJsonValid) break;
                }

                if (!isJsonValid) break;

                if (!bracketStack.empty()){
                    for (int i = 0 ;i < bracketStack.size(); i++)
                        System.out.print("\t");
                    line = sc.nextLine();
                }else{
                    break;
                }
            }

            if (isJsonValid) {

                JSONObject json;
                try {
                    json = JSONObject.parseObject(strJSON.toString());
                } catch (JSONException e) {
                    logger.logOnStderr("failed to parse json object");
                    continue;
                }

                long startTime = System.currentTimeMillis();
                String ret = client.sendJsonAndWaitResult(json);
                System.out.println("from server after " + (System.currentTimeMillis() - startTime) + "ms:\n" + toPrettyPrintJSON(ret));
                if (ret.equals("close")) {
                    break;
                }
            }
        }
        client.close();
    }
}
